package com.nakamachizu.service.mixi;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Calendar;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.Logger;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import net.arnx.jsonic.JSON;

import org.apache.commons.lang.RandomStringUtils;
import org.slim3.datastore.Datastore;
import org.slim3.util.TimeZoneLocator;

import com.google.appengine.api.urlfetch.HTTPHeader;
import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.HTTPRequest;
import com.google.appengine.api.urlfetch.HTTPResponse;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;
import com.nakamachizu.constant.MixiConstant;
import com.nakamachizu.model.mixi.BasePersonFriendModel;
import com.nakamachizu.model.mixi.PersonFriendModel;
import com.nakamachizu.model.mixi.PersonFriendsModel;
import com.nakamachizu.model.session.MixiSessionData;
import com.nakamachizu.util.ErrorsUtil;
import com.sun.mail.util.BASE64EncoderStream;


public class RestfulService {

    private static Logger log = Logger.getLogger(RestfulService.class.getName());
    
    public static final String TARGET_UID_ME = "@me";
    public static final String TARGET_TYPE_SELF = "@self";
    public static final String TARGET_TYPE_FRIENDS = "@friends";
    public static final String FORMAT_JSON = "json";
    public static final String FORMAT_XML = "atom";
    public static final int DEFAULT_COUNT = 30;
    public static final int DEFAULT_START_INDEX = 0;
    
    public static final String OAUTH_VERSION = "1.0";
    public static final String OAUTH_SIGNATURE_METHOD = "HMAC-SHA1";
    public static final String UTF8_CHARSET = "UTF-8";
    public static final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
    public static final String ENDPOINT = MixiConstant.RESTFULL_API_URL();
    public static final String PERSON_AND_FRIENDS_API = "/people";
    //private static final String REQUEST_URI = "/@me/@self";
    public static final String REQUEST_METHOD = "GET";
    
    private SecretKeySpec secretKeySpec;
    private Mac mac;
    private String timestamp;
    private String nonce;
    private String signature;
    private String viewerId;
    private String url;
    private SortedMap<String,String>param;
    
    private HTTPResponse response;
    
    public RestfulService(){
        try{
            String secKey = MixiConstant.CONSUMER_SECRET() + "&";
            byte[] secretyKeyBytes = secKey.getBytes(UTF8_CHARSET);
            secretKeySpec =
            new SecretKeySpec(secretyKeyBytes, HMAC_SHA1_ALGORITHM);
            mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
            mac.init(secretKeySpec);
        }catch(InvalidKeyException e){
            log.severe(ErrorsUtil.stackTrace(e));
        }catch(UnsupportedEncodingException e){
            log.severe(ErrorsUtil.stackTrace(e));
        }catch(NoSuchAlgorithmException e){
            log.severe(ErrorsUtil.stackTrace(e));
        }
    }
    
    public void makeSignature(String viewerId, String targetUid, String targetType, String format, Integer count, Integer startIndex, String ... fields){
        
        Calendar cal = Calendar.getInstance(TimeZoneLocator.get());
        timestamp = Long.toString(cal.getTime().getTime() / 1000);
        this.nonce = RandomStringUtils.randomAlphanumeric(32);
        this.viewerId = viewerId;
        
        String method = REQUEST_METHOD;
        this.url = ENDPOINT + PERSON_AND_FRIENDS_API + "/" + targetUid + "/" + targetType;
        
        this.param = new TreeMap<String,String>();
        this.param.put("oauth_consumer_key", MixiConstant.CONSUMER_KEY());
        this.param.put("oauth_nonce", this.nonce);
        this.param.put("oauth_signature_method", OAUTH_SIGNATURE_METHOD);
        this.param.put("oauth_timestamp", timestamp);
        this.param.put("oauth_version", OAUTH_VERSION);
        this.param.put("xoauth_requestor_id", this.viewerId);
        this.param.put("format", format);
        this.param.put("filterBy", "hasApp");
        if(fields != null && fields.length > 0){
            StringBuilder buf = new StringBuilder();
            for(int i=0; i<fields.length; i++){
                if(i != fields.length - 1){
                    buf.append(fields[i]).append(",");
                }else{
                    buf.append(fields[i]);
                }
            }
            this.param.put("fields", buf.toString());
        }
        if(count != null){
            this.param.put("count", count.toString());
        }
        if(startIndex != null){
            this.param.put("startIndex", startIndex.toString());
        }
        
        String paramStr = canonicalize(this.param);
        
        String strToSign = percentEncodeRfc3986(method) + "&" + percentEncodeRfc3986(this.url) + "&" + percentEncodeRfc3986(paramStr);
        
        //log.info("SIGN_SOURCE:" + strToSign);
        
        signature = hmac(strToSign);
        
        //log.info("SIGNATURE:" + signature);

    }
    
    public int request() throws IOException{
        
        SortedMap<String,String> map = this.getAdditionalParameters(this.param);
        String additional = this.canonicalize(map);
        //log.info("URL:" + this.url + "?" + additional);
        URL url = new URL(this.url + "?" + additional);
        URLFetchService ufs = URLFetchServiceFactory.getURLFetchService();
        HTTPRequest request = new HTTPRequest(url, HTTPMethod.GET);
        StringBuilder authHeader = new StringBuilder("OAuth ");
        authHeader.append("oauth_consumer_key=\"").append(MixiConstant.CONSUMER_KEY()).append("\",")
        .append("oauth_signature_method=\"").append(OAUTH_SIGNATURE_METHOD).append("\",")
        .append("oauth_signature=\"").append(signature).append("\",")
        .append("oauth_timestamp=\"").append(timestamp).append("\",")
        .append("oauth_nonce=\"").append(nonce).append("\",")
        .append("oauth_version=\"").append(OAUTH_VERSION).append("\"");
        //log.info("HEADER:" + authHeader.toString());
        request.addHeader(new HTTPHeader("Authorization", authHeader.toString()));
        
        this.response = ufs.fetch(request);
        
        return this.response.getResponseCode();
    }
    
    private String canonicalize(SortedMap<String, String> sortedParamMap)
    {
        if (sortedParamMap.isEmpty()) {
            return "";
        }

        StringBuffer buffer = new StringBuffer();
        Iterator<Map.Entry<String, String>> iter =
            sortedParamMap.entrySet().iterator();

        while (iter.hasNext()) {
            Map.Entry<String, String> kvpair = iter.next();
            buffer.append(percentEncodeRfc3986(kvpair.getKey()));
            buffer.append("=");
            buffer.append(percentEncodeRfc3986(kvpair.getValue()));
            if (iter.hasNext()) {
                buffer.append("&");
            }
        }
        String cannoical = buffer.toString();
        return cannoical;
    }
    
    private SortedMap<String,String> getAdditionalParameters(SortedMap<String,String> oauthIncludeMap){
        SortedMap<String,String> map = new TreeMap<String, String>();
        for(Map.Entry<String, String> e : oauthIncludeMap.entrySet()){
            map.put(e.getKey(), e.getValue());
        }
        map.remove("oauth_consumer_key");
        map.remove("oauth_signature_method");
        map.remove("oauth_timestamp");
        map.remove("oauth_nonce");
        map.remove("oauth_version");
        
        return map;
    }
    
    private String percentEncodeRfc3986(String s) {
        String out;
        try {
            out = URLEncoder.encode(s, UTF8_CHARSET)
            .replace("+", "%20")
            .replace("*", "%2A")
            .replace("%7E", "~");
        } catch (UnsupportedEncodingException e) {
            out = s;
        }
        return out;
    }
    
    private String hmac(String stringToSign) {
        String signature = null;
        byte[] data;
        byte[] rawHmac;
        try {
            data = stringToSign.getBytes(UTF8_CHARSET);
            rawHmac = mac.doFinal(data);
            signature = new String(BASE64EncoderStream.encode(rawHmac), "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(UTF8_CHARSET + " is unsupported!", e);
        }
        return signature;
    }
    
   
    public String getContents() throws UnsupportedEncodingException{
        String result = new String(this.response.getContent(), "UTF-8");
        //log.info(result);
        return result;
    }
    
    public List<HTTPHeader> getHttpHeader(){
        return this.response.getHeaders();
    }
    
    public String getHttpHeaderString(){
        StringBuilder buf = new StringBuilder("HTTP Headers:\n");
        for(HTTPHeader header : this.response.getHeaders()){
            buf.append(header.getName()).append(":").append(header.getValue()).append("\n");
        }
        return buf.toString();
    }
    
    public String getRequestUrl(){
        SortedMap<String,String> map = this.getAdditionalParameters(this.param);
        String additional = this.canonicalize(map);
        return this.url + "?" + additional;
    }
    
    public boolean isFriend(MixiSessionData session) throws IOException{
        if(session.getViewerId().equals(session.getOwnerId())){
            session.setFriend(new Boolean(true));
        }
        if(session.getFriend() == null){
            String targetType = TARGET_TYPE_FRIENDS + "/" + session.getOwnerId();
            this.makeSignature(session.getViewerId(), TARGET_UID_ME, targetType, FORMAT_JSON, DEFAULT_COUNT, DEFAULT_START_INDEX);
            if(this.request() == 200){
                session.setFriend(new Boolean(true));
            }else{
                session.setFriend(new Boolean(false));
            }
            Datastore.put(session);
        }
        return session.getFriend().booleanValue();
    }
    
    public PersonFriendsModel getPersonFriendsModel() throws UnsupportedEncodingException{
        
        PersonFriendsModel result = new PersonFriendsModel();
        result.setTotalResults(0);
        result.setStartIndex(0);
        log.info("json contents:" + this.getContents());
        BasePersonFriendModel bpfm = JSON.decode(this.getContents(), BasePersonFriendModel.class);
        if(bpfm.getTotalResults() == 1){
            PersonFriendModel pfm = JSON.decode(this.getContents(), PersonFriendModel.class);
            Map<String,String>[] arr = new TreeMap[1];
            try{
                arr[0] = pfm.getEntry();
                result.setEntry(arr);
            }catch(ArrayStoreException e){
                // 1件でも配列で来る場合があるらしい
                result = JSON.decode(this.getContents(), PersonFriendsModel.class);
            }
        }else if(bpfm.getTotalResults() > 1){
            result = JSON.decode(this.getContents(), PersonFriendsModel.class);
        }
        
        return result;
    }
    
}
